/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

package com.aduib.boot.common.compiler.support;

import com.aduib.boot.common.util.ClassUtils;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @description: JavassistClassBuilder
 * @author: zzh
 * @date: 2021/10/15 15:57
 */
public class JavassistClassBuilder {
  private String className;

  private String superClassName = "java.lang.Object";

  private List<String> imports = new ArrayList<>();

  private Map<String, String> fullNames = new HashMap<>();

  private List<String> interfaces = new ArrayList<>();

  private List<String> constructors = new ArrayList<>();

  private List<String> fields = new ArrayList<>();

  private List<String> methods = new ArrayList<>();

  public String getClassName() {
    return className;
  }

  public void setClassName(String className) {
    this.className = className;
  }

  public String getSuperClassName() {
    return superClassName;
  }

  public void setSuperClassName(String superClassName) {
    this.superClassName = getQualifiedClassName(superClassName);
  }

  public List<String> getImports() {
    return imports;
  }

  public Map<String, String> getFullNames() {
    return fullNames;
  }

  public List<String> getInterfaces() {
    return interfaces;
  }

  public List<String> getConstructors() {
    return constructors;
  }

  public List<String> getFields() {
    return fields;
  }

  public List<String> getMethods() {
    return methods;
  }

  public void addImports(String pkg) {
    int pi = pkg.lastIndexOf('.');
    if (pi > 0) {
      String pkgName = pkg.substring(0, pi);
      this.imports.add(pkgName);
      if (!pkg.endsWith(".*")) {
        fullNames.put(pkg.substring(pi + 1), pkg);
      }
    }
  }

  public void addInterface(String interfaceName) {
    interfaces.add(getQualifiedClassName(interfaceName));
  }

  public void addConstructor(String constructor) {
    constructors.add(constructor);
  }

  public void addField(String field) {
    fields.add(field);
  }

  public void addMethod(String method) {
    methods.add(method);
  }

  protected String getQualifiedClassName(String className) {
    if (className.contains(".")) {
      return className;
    }

    if (fullNames.containsKey(className)) {
      return fullNames.get(className);
    }

    return ClassUtils.forName(imports.toArray(new String[0]), className).getName();
  }

  public CtClass build(ClassLoader classLoader) throws NotFoundException, CannotCompileException {
    ClassPool pool = new ClassPool(true);
    pool.appendClassPath(new LoaderClassPath(classLoader));

    // create class
    CtClass ctClass = pool.makeClass(className, pool.get(superClassName));

    // add imported packages
    imports.forEach(pool::importPackage);
    // add implemented interfaces
    for (String iface : interfaces) {
      ctClass.addInterface(pool.get(iface));
    }

    // add constructors
    for (String constructor : constructors) {
      ctClass.addConstructor(CtNewConstructor.make(constructor, ctClass));
    }

    // add fields
    for (String field : fields) {
      ctClass.addField(CtField.make(field, ctClass));
    }

    // add methods
    for (String method : methods) {
      ctClass.addMethod(CtNewMethod.make(method, ctClass));
    }

    return ctClass;
  }
}
